///<reference path="../typings/disco.d.ts" />

import model = require('index.model');
import jayDisco = require('disco');
import common = require('common');
import eventmgr = require('eventmgr');

export class Context {
	public mdl: model.Model;
	public emgr: eventmgr.EventMgr;
}

class MediatorPromiseData<T> {
	public then: (x: T) => void;
	public fail: () => void;
	
	public promise: MediatorPromise<T> = new MediatorPromiseImpl<T>(this);
	
	public fireThen = (x: T) => {
		this.thenArg = x;
		this.update();
	}
	
	public fireFail = () => {
		this.failFired = true;
		this.update();
	}
	
	private thenArg: T = null;
	private failFired: boolean = false;
	
	public update = () => {
		if(this.thenArg && this.then) {
			this.then(this.thenArg);
			this.thenArg = null;
		}
		if(this.failFired && this.fail) {
			this.failFired = false;
			this.fail();
		}
	}
}

export interface MediatorPromise<T> {
	then: ( handler: (x: T) => void ) => MediatorPromise<T>;
	fail: ( handler: () => void ) => MediatorPromise<T>;
}

class MediatorPromiseImpl<T> implements MediatorPromise<T> {
	public then( handler: (x: T) => void ): MediatorPromise<T> {
		this.data.then = handler;
		this.data.update();
		return this;
	}
	public fail( handler: () => void ): MediatorPromise<T> {
		this.data.fail = handler;
		this.data.update();
		return this;
	}
	
	private data: MediatorPromiseData<T>;
	
	constructor(data: MediatorPromiseData<T>) {
		this.data = data;
	}
}

export interface QkCommentableMediator {
	getKommentar(id: number, parent: model.QkCommentable, out?: model.Kommentar): MediatorPromise<model.Kommentar>;
	getKommentare(parent: model.QkCommentable): MediatorPromise<model.QkCommentable>;
}

export interface Mediator extends QkCommentableMediator {
	setAuthDataFunction(func: () => any): void;

	getKonsenskiste(id: number, out?: model.Konsenskiste): MediatorPromise<model.Konsenskiste>;
}

export class DiscoMediator implements Mediator {
	private discoContext: jayDisco.Context;
	
	public setAuthDataFunction(func: () => any) {
		jayDisco.AuthData = func;
	}

	public getKonsenskiste(id: number, out?: model.Konsenskiste): MediatorPromise<model.Konsenskiste> {
		var promiseData = new MediatorPromiseData<model.KonsenskisteImpl>();
		this.queryKonsenskiste(id).then(rsp => {
			var kk: model.Konsenskiste = out || new model.KonsenskisteImpl();
			this.parseKonsenskiste(rsp[0], kk);
			promiseData.fireThen(kk);
		});
		return promiseData.promise;
	}
	
	public getTopic(id: number, out?: model.Topic): MediatorPromise<model.Topic> {
		var promiseData = new MediatorPromiseData<model.Topic>();
		var tpc = out || new model.Topic();
		common.Callbacks.atOnce([
			ready => {
				this.queryTopic(id).then(rsp => {
					this.parseTopic(rsp[0], tpc);
					promiseData.fireThen(tpc);
					ready();
				});
			},
			ready => {
				this.queryTopicChildren(id).then(rsp => {
					this.parseTopicChildren(rsp, tpc);
					//promiseData.fireThen(tpc);
					ready();
				});
			},
			ready => {
				this.queryTopicKks(id).then(rsp => {
					this.parseTopicKks(rsp, tpc);
					ready();
				});
			}
		], () => { /*promiseData.fireThen(tpc)*/ });
		return promiseData.promise;
	}
	
	public getKommentar(id: number, parent: model.QkCommentable, out?: model.Kommentar): MediatorPromise<model.Kommentar> {
		var promiseData = new MediatorPromiseData<model.Kommentar>();
		this.queryKommentar(id).then(rsp => {
			var cmt = out || new model.Kommentar();
			this.parseKommentar(rsp[0], parent, cmt);
			promiseData.fireThen(cmt);
		});
		return promiseData.promise;
	}
	
	public getKommentare(parent: model.QkCommentable): MediatorPromise<model.QkCommentable> {
		var promiseData = new MediatorPromiseData<model.QkCommentable>();
		parent.comments([]);
		/*this.queryParentKommentare(parent).then(rsp => {
			rsp.forEach(rawCmt => {
				var cmt = new model.Kommentar();
				this.parseKommentar(rawCmt, parent, cmt);
				parent.comments.push(cmt);
			});
			promiseData.fireThen(parent);
		});*/
		this.queryCommentable(parent.id).then(rsp => {
			this.parseQkCommentableModule(rsp[0], parent);
			promiseData.fireThen(parent);
		})
		return promiseData.promise;
	}
	
	private queryKonsenskiste(id: number): $data.IPromise<Disco.Ontology.Post[]> {
		return this.discoContext.Posts.filter(function(it) { return it.Id == this.Id }, { Id: id })
		.include("ReferredFrom.Referrer.Content")
		.include("ReferredFrom.Referrer.Ratings")
		.include("ReferredFrom.Referrer.Ratings.ModifiedBy.Author")
		.include("ReferredFrom.Referrer.ReferredFrom")
		.include("ReferredFrom.Referrer.ReferredFrom.ReferenceType.Description")
		.include("ReferredFrom.Referrer.RefersTo.Referree.Content")
		.include("ReferredFrom.Referrer.RefersTo.ReferenceType.Description")
		.include("ReferredFrom.ReferenceType.Description")
		.include("RefersTo.Referree")
		.include("RefersTo.Referree.Ratings")
		.include("RefersTo.Referree.Ratings.ModifiedBy.Author")
		.include("RefersTo.ReferenceType")
		.include("RefersTo.ReferenceType.Description")
		.include("Content")
		.include("Ratings")
		.include("Ratings.ModifiedBy.Author")
		.toArray();
	}
	
	private queryCommentable(id: number): $data.IPromise<Disco.Ontology.Post[]> {
		return this.discoContext.Posts.filter(function(it) { return it.Id == this.Id }, { Id: id })
		.include("ReferredFrom.ReferenceType.Description")
		.include("ReferredFrom.Referrer.Content")
		.include("ReferredFrom.Referrer.Ratings")
		.include("ReferredFrom.Referrer.Ratings.ModifiedBy.Author")
		.toArray();
	}
	
	private queryTopic(id: number): $data.IPromise<Disco.Ontology.Post[]> {
		return this.discoContext.Posts.filter(function(it) { return it.Id == this.Id }, { Id: id })
		.include("Content")
		.include("ReferredFrom.Referrer.Content")
		.include("ReferredFrom.ReferenceType.Description")
		.toArray();
	}
	
	private queryTopicChildren(id: number): $data.IPromise<Disco.Ontology.Post[]> {
		var parentlessFilter = this.discoContext.PostReferences.filter(function(it) { return it.ReferenceType.Description.Name != "Child" });
		var subFilter = this.discoContext.PostReferences.filter(function(it) { return it.ReferenceType.Description.Name == "Child" && it.Referree.Id == this.id }, { id: id });
		
		return id
			? this.discoContext.Posts.filter(function(it) { return it.PostType.Description.Name == "Topic" && it.RefersTo.some(this.subFilter) }, 
			  { subFilter: subFilter })
			  .include("Content")
			  .toArray()
			: this.discoContext.Posts.filter(function(it) { return it.PostType.Description.Name == "Topic" && it.RefersTo.every(this.parentlessFilter) },
			  { parentlessFilter: parentlessFilter })
			  .include("Content")
			  .toArray();
	}
	
	private queryTopicKks(id: number): $data.IPromise<Disco.Ontology.Post[]> {
		var dependenceFilter = this.discoContext.PostReferences.filter(function(it) { return it.Referree.Id == this.id }, { id: id });
		var kaRefFilter = this.discoContext.PostReferences.filter(function(it) { return it.ReferenceType.Description.Name == 'Part' });
		
		return this.discoContext.Posts.filter(function(it) { return it.RefersTo.some(this.dependenceFilter) && it.ReferredFrom.some(this.kaRefFilter) }, 
		{ dependenceFilter: dependenceFilter, kaRefFilter: kaRefFilter })
		.include("Content")
		.toArray();
	}
	
	private queryKommentare(ids: number[]): $data.IPromise<Disco.Ontology.Post[]> {
		return this.discoContext.Posts.filter(ids.map(function(x) { return "it.Id == " + x }).join('||'))
		.include("Content")
		.include("Ratings.ModifiedBy.Author")
		.toArray();
	}
	
	private queryParentKommentare(parent: model.QkCommentable): $data.IPromise<Disco.Ontology.Post[]> {
		var filter = this.discoContext.PostReferences.filter("it.ReferreeId == this.Id", { Id: parent.id });
		return this.discoContext.Posts.filter("it.RefersTo.some(this.filter)", { filter: filter })
		.include("Content")
		.include("Ratings.ModifiedBy.Author")
		.toArray();
	}
	
	private queryKommentar(id: number): $data.IPromise<Disco.Ontology.Post[]> {
		return this.queryKommentare([id]);
	}
	
	private parseKonsenskiste(raw: Disco.Ontology.Post, out: model.Konsenskiste): void {
		this.parseQkElement(raw, out);
		this.parseQkCommentableModule(raw, out);
		this.parseQkWithContextModule(raw, out);
		
		out.children([]);
		
		raw.ReferredFrom.forEach(reference => {
			if(reference.ReferenceType.Description.Name == 'Part') {
				var rawKa = reference.Referrer;
				var ka = new model.KernaussageImpl();
				
				this.parseKernaussage(rawKa, out, ka);
				out.children.push(ka);
			}
		});
	}
	
	private parseTopic(raw: Disco.Ontology.Post, out: model.Topic): void {
		out.id = parseInt(raw.Id);
		out.title(raw.Content.Title);
		out.text(raw.Content.Text);
		
		/*var children = common.Coll.where(raw.ReferredFrom, ref => ref.ReferenceType.Description.Name == 'Child').map(r => r.Referrer);
		out.children(children.map(c => {
			var t = new model.Topic(c.Id);
			t.title(c.Content.Title);
			t.text(c.Content.Text);
			return t;
		}));*/
	}
	
	parseTopicChildren(raw: Disco.Ontology.Post[], out: model.Topic): void {
		out.children(raw.map(c => {
			var t = new model.Topic(parseInt(c.Id));
			this.parseTopic(c, t);
			return t;
		}));
	}
	
	parseTopicKks(raw: Disco.Ontology.Post[], out: model.Topic): void {
		out.kks(raw.map(c => {
			var k = new model.KonsenskisteImpl(parseInt(c.Id));
			k.title(c.Content.Title);
			k.text(c.Content.Text);
			return k;
		}));
	}
	
	private parseKernaussage(raw: Disco.Ontology.Post, parent: model.QkParent, out: model.Kernaussage): void {
		this.parseQkElement(raw, out);
		this.parseQkChildModule(raw, parent, out);
		this.parseQkCommentableModule(raw, out);
		this.parseQkWithContextModule(raw, out);
	}
	
	private parseKommentar(raw: Disco.Ontology.Post, parent: model.QkCommentable, out: model.Kommentar): void {
		this.parseQkElement(raw, out);
		out.parent(parent);
	}
	
	private parseQkChildModule(raw: Disco.Ontology.Post, parent: model.QkParent, out: model.QkChild): void {
		out.parent(parent);
	}
	
	private parseQkCommentableKommentare(raw: Disco.Ontology.Post[], parent: model.QkCommentable, out: model.QkCommentable): void {
		out.comments().forEach(cmt => {
			var rawCmt = common.Coll.single(raw, c => c.Id == cmt.id);
			if(rawCmt) this.parseKommentar(rawCmt, parent, cmt);
		});
	}
	
	private parseQkCommentableModule(raw: Disco.Ontology.Post, out: model.QkCommentable): void {
		raw.ReferredFrom.forEach(reference => {
			if(['Part', 'Child', 'Context'].indexOf(reference.ReferenceType.Description.Name) == -1) {
				var cmt = new model.Kommentar();
				cmt.id = parseInt(reference.ReferrerId);
				
				if(reference.Referrer && reference.Referrer.Content && reference.Referrer.Ratings) {
					//cmt.title(reference.Referrer.Content.Title);
					//cmt.text(reference.Referrer.Content.Text);
					this.parseKommentar(reference.Referrer, out, cmt);
				}
				
				out.comments.push(cmt);
			}
		});
		out.comments.sort((a, b) => a.id - b.id);
	}
	
	private parseQkWithContextModule(raw: Disco.Ontology.Post, out: model.QkWithContext): void {
		var cxts = common.Coll.where(raw.RefersTo, ref => ref.ReferenceType.Description.Name == 'Context').map(ref => ref.Referree.Content.Text);
		out.context(cxts[0]);
		if(cxts.length > 1) console.warn('There is more than a single context for post ' + raw.Id + '!');
	}
	
	private parseQkElement(raw: Disco.Ontology.Post, out: model.QkElement): void {
		out.id = parseInt(raw.Id);
		out.title(raw.Content.Title);
		out.text(raw.Content.Text);
		
		if(raw.Ratings) {
			var myRatings: Disco.Ontology.Rating[] = common.Coll.where(raw.Ratings, r => r.ModifiedBy.Author.Alias == this.cxt.mdl.user());
			if(myRatings.length > 1) console.warn("There's more than one Rating per Post and User!");
			out.rating(RatingMapper.fromDisco(myRatings[0]));
			
			out.stronglikeSum(common.Coll.where(raw.Ratings, r => RatingMapper.fromDisco(r) == 'stronglike').length);
			out.likeSum(common.Coll.where(raw.Ratings, r => RatingMapper.fromDisco(r) == 'like').length);
			out.neutralSum(common.Coll.where(raw.Ratings, r => RatingMapper.fromDisco(r) == 'neutral').length);
			out.dislikeSum(common.Coll.where(raw.Ratings, r => RatingMapper.fromDisco(r) == 'dislike').length);
			out.strongdislikeSum(common.Coll.where(raw.Ratings, r => RatingMapper.fromDisco(r) == 'strongdislike').length);
		}
	}
	
	private submitRating(postId: number, ratingVal: string): MediatorPromise<string> {
		var promiseData = new MediatorPromiseData<string>();
	
		var discoRating: number = RatingMapper.toDisco(ratingVal);
		var user = 'anonymous';
		this.discoContext.Ratings.filter("it.ModifiedBy.Author.Alias == 'anonymous' && it.PostId == this.PostId", { PostId: postId }).toArray().then(rsp => {
			var rating: Disco.Ontology.Rating;
			if(rsp.length > 0) {
				rating = rsp[rsp.length-1];
			}
			else {
				rating = new Disco.Ontology.Rating();
				this.discoContext.Ratings.add(rating);
			}
			rating.Score = discoRating;
			rating.PostId = postId.toString();
			rating.UserId = '12';
			(<any>rating).save().then(() => promiseData.fireThen(ratingVal));
		});
		
		return promiseData.promise;
	}
	
	private submitKk(title: string, text: string, parent: model.Topic, out: model.Konsenskiste) {
		var cnt: Disco.Ontology.Content;
		var post: Disco.Ontology.Post;
		var topicRef: Disco.Ontology.PostReference;
		common.Callbacks.batch([
			r => {
				cnt = new Disco.Ontology.Content({ CultureId: '2', Title: title, Text: text })
				this.discoContext.Content.add(cnt)
				this.discoContext.saveChanges(r)
			},
			r => {
				post = new Disco.Ontology.Post({ ContentId: cnt.Id.toString(), PostTypeId: '2' })
				this.discoContext.Posts.add(post)
				this.discoContext.saveChanges(r)
			},
			r => {
				topicRef = new Disco.Ontology.PostReference({ ReferenceTypeId: '2', ReferrerId: post.Id.toString(), ReferreeId: parent.id.toString() });
				this.discoContext.PostReferences.add(topicRef);
				this.discoContext.saveChanges(r)
			},
			r => {
				out.title(cnt.Title)
				out.text(cnt.Text)
			}
		])
	}
	
	private submitKa(title: string, text: string, context: string, parent: model.Konsenskiste, out: model.Kernaussage) {
		var content, cxtContent: Disco.Ontology.Content;
		var post, cxtPost: Disco.Ontology.Post;
		var reference, cxtReference: Disco.Ontology.PostReference;
		common.Callbacks.batch([
			r => {
				content = new Disco.Ontology.Content({ Title: title, Text: text, CultureId: "2" });
				this.discoContext.Content.add(content);
				this.discoContext.saveChanges().then(r);
			},
			r => {
				post = new Disco.Ontology.Post({ ContentId: content.Id.toString(), PostTypeId: "2" });
				this.discoContext.Posts.add(post);
				this.discoContext.saveChanges().then(r);
			},
			r => {
				reference = new Disco.Ontology.PostReference({ ReferenceTypeId: "11" /* Child */, ReferrerId: post.Id.toString(), ReferreeId: parent.id.toString() });
				this.discoContext.PostReferences.add(reference);
				this.discoContext.saveChanges().then(r);
			},
			r => {
				cxtContent = new Disco.Ontology.Content({ Title: "(ein Klärtext)", Text: context, CultureId: "2" });
				this.discoContext.Content.add(cxtContent);
				this.discoContext.saveChanges().then(r);
			},
			r => {
				cxtPost = new Disco.Ontology.Post({ ContentId: cxtContent.Id.toString(), PostTypeId: "2" });
				this.discoContext.Posts.add(cxtPost);
				this.discoContext.saveChanges().then(r);
			},
			r => {
				cxtReference = new Disco.Ontology.PostReference({ ReferenceTypeId: "10" /* Context */, ReferreeId: cxtPost.Id.toString(), ReferrerId: post.Id.toString() });
				this.discoContext.PostReferences.add(cxtReference);
				this.discoContext.saveChanges().then(r);
			},
			r => {
				this.cxt.emgr.kkNeeded({ id: parent.id, out: parent });
			}
		]);
	}
	
	private submitCmt(text: string, parent: model.QkMainElement) {
		var cnt: Disco.Ontology.Content;
		var post: Disco.Ontology.Post;
		var ref: Disco.Ontology.PostReference;
		common.Callbacks.batch([
			r => {
				cnt = new Disco.Ontology.Content({ CultureId: '2', Text: text });
				this.discoContext.Content.add(cnt);
				this.discoContext.saveChanges(r);
			},
			r => {
				post = new Disco.Ontology.Post({ ContentId: cnt.Id.toString(), PostTypeId: '2' });
				this.discoContext.Posts.add(post);
				this.discoContext.saveChanges(r);
			},
			r => {
				ref = new Disco.Ontology.PostReference({ ReferenceTypeId: '2', ReferrerId: post.Id.toString(), ReferreeId: parent.id.toString() });
				this.discoContext.PostReferences.add(ref);
				this.discoContext.saveChanges(r);
			},
			r => {
				this.cxt.emgr.commentsNeeded({ parent: parent });
			}
		]);
	}
	
	private removeKa(ka: model.Kernaussage) {
		//TODO: Verknüpfung entfernen oder alles löschen?
		var refKaKk;
		common.Callbacks.batch([
			r => {
				 this.discoContext.PostReferences.filter('it.ReferrerId == this.ReferrerId && this.ReferreeId == this.ReferreeId && it.ReferenceTypeId == 11',
				 	{ ReferrerId: ka.id, ReferreeId: ka.parent().id })
				 	.toArray().then(function(rsp) {
				 		refKaKk = rsp[0];
				 		r();
				 	});
			},
			r => {
				if(refKaKk) {
					refKaKk.remove();
					this.discoContext.saveChanges().then(r);
				}
			},
			r => {
				this.cxt.emgr.kkNeeded({ id: ka.parent().id, out: ka.parent() });
			}
		]);
	}
	
	private removeCmt(cmt: model.QkComment) {
		//TODO: Verknüpfung entfernen oder alles löschen?
		var refCmtKa;
		common.Callbacks.batch([
			r => {
				 this.discoContext.PostReferences.filter('it.ReferrerId == this.ReferrerId && this.ReferreeId == this.ReferreeId',
				 	{ ReferrerId: cmt.id, ReferreeId: cmt.parent().id })
				 	.toArray().then(function(rsp) {
				 		refCmtKa = rsp[0];
				 		r();
				 	});
			},
			r => {
				if(refCmtKa) {
					refCmtKa.remove();
					this.discoContext.saveChanges().then(r);
				}
			},
			r => {
				this.cxt.emgr.commentsNeeded({ parent: cmt.parent() });
			}
		]);
	}
	
	private connectEventMgr() {
		this.cxt.emgr.registerKkNeeded(args => { this.getKonsenskiste(args.id, args.out); });
		this.cxt.emgr.registerTopicNeeded(args => { this.getTopic(args.id, args.out); });
		this.cxt.emgr.registerCommentsNeeded(args => { this.getKommentare(args.parent); });
		this.cxt.emgr.registerRated(args => { this.submitRating(args.postId, args.rating).then(args.then) });
		this.cxt.emgr.registerKkSubmitted(args => { this.submitKk(args.title, args.text, args.parent, args.out) });
		this.cxt.emgr.registerKaSubmitted(args => { this.submitKa(args.title, args.text, args.context, args.parent, args.out) });
		this.cxt.emgr.registerCmtSubmitted(args => { this.submitCmt(args.text, args.parent) });
		this.cxt.emgr.registerRemoveKa(args => { this.removeKa(args.ka) });
		this.cxt.emgr.registerRemoveCmt(args => { this.removeCmt(args.cmt) });
	}
	
	private disconnectEventMgr() {
		this.cxt.emgr.registerRemoveCmt(null);
		this.cxt.emgr.registerRemoveKa(null);
		this.cxt.emgr.registerCmtSubmitted(null);
		this.cxt.emgr.registerKaSubmitted(null);
		this.cxt.emgr.registerKkSubmitted(null);
		this.cxt.emgr.registerRated(null);
		this.cxt.emgr.registerCommentsNeeded(null);
		this.cxt.emgr.registerTopicNeeded(null);
		this.cxt.emgr.registerKkNeeded(null);
	}
	
	private cxt: Context;
	
	constructor(discoUri: string, cxt: Context) {
		this.cxt = cxt;
		this.discoContext = new jayDisco.Context(discoUri);
		this.connectEventMgr();
	}
}

class RatingMapper {
	private static strings: string[] = ['strongdislike', 'dislike', 'neutral', 'like', 'stronglike'];

	public static fromDisco(rating: Disco.Ontology.Rating) {
		if(rating) {
			return RatingMapper.strings[Math.round(rating.Score / 3) + 2];
		}
		else {
			return 'none';
		}
	}
	
	public static toDisco(rating: string) {
		var index = RatingMapper.strings.indexOf(rating);
		if(index >= 0) {
			return (index - 2) * 3;
		}
		return null;
	}
}